/*
 * brin.c
 *		Postgres 的 BRIN 索引实现
 *
 * 有关详细信息，请参见 src/backend/access/brin/README。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/access/brin/brin.c
 *
 * TODO
 *		* ScalarArrayOpExpr (amsearcharray -> SK_SEARCHARRAY)
 */
#include "postgres.h"

#include "access/brin.h"
#include "access/brin_page.h"
#include "access/brin_pageops.h"
#include "access/brin_xlog.h"
#include "access/relation.h"
#include "access/reloptions.h"
#include "access/relscan.h"
#include "access/table.h"
#include "access/tableam.h"
#include "access/xloginsert.h"
#include "catalog/index.h"
#include "catalog/pg_am.h"
#include "commands/vacuum.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/autovacuum.h"
#include "storage/bufmgr.h"
#include "storage/freespace.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/index_selfuncs.h"
#include "utils/memutils.h"
#include "utils/rel.h"


/*
 * 我们在初始构建 BRIN 索引时使用 BrinBuildState。
 * 运行状态保存在 BrinMemTuple 中。
 */
typedef struct BrinBuildState
{
	Relation	bs_irel;
	int			bs_numtuples;
	Buffer		bs_currentInsertBuf;
	BlockNumber bs_pagesPerRange;
	BlockNumber bs_currRangeStart;
	BrinRevmap *bs_rmAccess;
	BrinDesc   *bs_bdesc;
	BrinMemTuple *bs_dtuple;
} BrinBuildState;

/*
 * 在索引扫描中用作“不透明”的结构
 */
typedef struct BrinOpaque
{
	BlockNumber bo_pagesPerRange;
	BrinRevmap *bo_rmAccess;
	BrinDesc   *bo_bdesc;
} BrinOpaque;

#define BRIN_ALL_BLOCKRANGES	InvalidBlockNumber

static BrinBuildState *fc_initialize_brin_buildstate(Relation fc_idxRel,
												  BrinRevmap *fc_revmap, BlockNumber fc_pagesPerRange);
static void fc_terminate_brin_buildstate(BrinBuildState *fc_state);
static void fc_brinsummarize(Relation fc_index, Relation fc_heapRel, BlockNumber fc_pageRange,
						  bool fc_include_partial, double *fc_numSummarized, double *fc_numExisting);
static void fc_form_and_insert_tuple(BrinBuildState *fc_state);
static void fc_union_tuples(BrinDesc *fc_bdesc, BrinMemTuple *fc_a,
						 BrinTuple *fc_b);
static void fc_brin_vacuum_scan(Relation fc_idxrel, BufferAccessStrategy fc_strategy);
static bool fc_add_values_to_range(Relation fc_idxRel, BrinDesc *fc_bdesc,
								BrinMemTuple *fc_dtup, Datum *fc_values, bool *fc_nulls);
static bool fc_check_null_keys(BrinValues *fc_bval, ScanKey *fc_nullkeys, int fc_nnullkeys);

/*
 * BRIN 处理函数：返回带有访问方法参数和回调的 IndexAmRoutine。
 */
Datum brinhandler(PG_FUNCTION_ARGS)
{
	IndexAmRoutine *fc_amroutine = makeNode(IndexAmRoutine);

	fc_amroutine->amstrategies = 0;
	fc_amroutine->amsupport = BRIN_LAST_OPTIONAL_PROCNUM;
	fc_amroutine->amoptsprocnum = BRIN_PROCNUM_OPTIONS;
	fc_amroutine->amcanorder = false;
	fc_amroutine->amcanorderbyop = false;
	fc_amroutine->amcanbackward = false;
	fc_amroutine->amcanunique = false;
	fc_amroutine->amcanmulticol = true;
	fc_amroutine->amoptionalkey = true;
	fc_amroutine->amsearcharray = false;
	fc_amroutine->amsearchnulls = true;
	fc_amroutine->amstorage = true;
	fc_amroutine->amclusterable = false;
	fc_amroutine->ampredlocks = false;
	fc_amroutine->amcanparallel = false;
	fc_amroutine->amcaninclude = false;
	fc_amroutine->amusemaintenanceworkmem = false;
	fc_amroutine->amparallelvacuumoptions =
		VACUUM_OPTION_PARALLEL_CLEANUP;
	fc_amroutine->amkeytype = InvalidOid;

	fc_amroutine->ambuild = brinbuild;
	fc_amroutine->ambuildempty = brinbuildempty;
	fc_amroutine->aminsert = brininsert;
	fc_amroutine->ambulkdelete = brinbulkdelete;
	fc_amroutine->amvacuumcleanup = brinvacuumcleanup;
	fc_amroutine->amcanreturn = NULL;
	fc_amroutine->amcostestimate = brincostestimate;
	fc_amroutine->amoptions = brinoptions;
	fc_amroutine->amproperty = NULL;
	fc_amroutine->ambuildphasename = NULL;
	fc_amroutine->amvalidate = brinvalidate;
	fc_amroutine->amadjustmembers = NULL;
	fc_amroutine->ambeginscan = brinbeginscan;
	fc_amroutine->amrescan = brinrescan;
	fc_amroutine->amgettuple = NULL;
	fc_amroutine->amgetbitmap = bringetbitmap;
	fc_amroutine->amendscan = brinendscan;
	fc_amroutine->ammarkpos = NULL;
	fc_amroutine->amrestrpos = NULL;
	fc_amroutine->amestimateparallelscan = NULL;
	fc_amroutine->aminitparallelscan = NULL;
	fc_amroutine->amparallelrescan = NULL;

	PG_RETURN_POINTER(fc_amroutine);
}

/*
 * 堆中正在插入一个元组。为了保持 brin 索引的最新状态，
 * 我们需要获取相关的索引元组并将其存储的值与新元组的值进行比较。
 * 如果元组值与摘要元组不一致，我们需要更新索引元组。
 *
 * 如果启用了自动摘要，检查我们是否需要对之前的页面范围进行摘要。
 *
 * 如果当前范围尚未摘要（即 revmap 对其返回 NULL），
 * 则对该元组没有任何操作。
 */
bool brininsert(Relation fc_idxRel, Datum *fc_values, bool *fc_nulls,
		   ItemPointer fc_heaptid, Relation fc_heapRel,
		   IndexUniqueCheck fc_checkUnique,
		   bool fc_indexUnchanged,
		   IndexInfo *fc_indexInfo)
{
	BlockNumber fc_pagesPerRange;
	BlockNumber fc_origHeapBlk;
	BlockNumber fc_heapBlk;
	BrinDesc   *fc_bdesc = (BrinDesc *) fc_indexInfo->ii_AmCache;
	BrinRevmap *fc_revmap;
	Buffer		fc_buf = InvalidBuffer;
	MemoryContext fc_tupcxt = NULL;
	MemoryContext fc_oldcxt = CurrentMemoryContext;
	bool		fc_autosummarize = BrinGetAutoSummarize(fc_idxRel);

	fc_revmap = brinRevmapInitialize(fc_idxRel, &fc_pagesPerRange, NULL);

	/*
	 * origHeapBlk 是插入发生的块编号。heapBlk
	 * 是相应页面范围中的第一个块。
	 */
	fc_origHeapBlk = ItemPointerGetBlockNumber(fc_heaptid);
	fc_heapBlk = (fc_origHeapBlk / fc_pagesPerRange) * fc_pagesPerRange;

	for (;;)
	{
		bool		fc_need_insert = false;
		OffsetNumber fc_off;
		BrinTuple  *fc_brtup;
		BrinMemTuple *fc_dtup;

		CHECK_FOR_INTERRUPTS();

		/*
		 * 如果启用了自动摘要，并且我们刚刚将第一个
		 * 元组插入到新非第一页面范围的第一个块中，请请求对
		 * 上一个范围进行摘要运行。
		 */
		if (fc_autosummarize &&
			fc_heapBlk > 0 &&
			fc_heapBlk == fc_origHeapBlk &&
			ItemPointerGetOffsetNumber(fc_heaptid) == FirstOffsetNumber)
		{
			BlockNumber fc_lastPageRange = fc_heapBlk - 1;
			BrinTuple  *fc_lastPageTuple;

			fc_lastPageTuple =
				brinGetTupleForHeapBlock(fc_revmap, fc_lastPageRange, &fc_buf, &fc_off,
										 NULL, BUFFER_LOCK_SHARE, NULL);
			if (!fc_lastPageTuple)
			{
				bool		fc_recorded;

				fc_recorded = AutoVacuumRequestWork(AVW_BRINSummarizeRange,
												 RelationGetRelid(fc_idxRel),
												 fc_lastPageRange);
				if (!fc_recorded)
					ereport(LOG,
							(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
							 errmsg("request for BRIN range summarization for index \"%s\" page %u was not recorded",
									RelationGetRelationName(fc_idxRel),
									fc_lastPageRange)));
			}
			else
				LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
		}

		fc_brtup = brinGetTupleForHeapBlock(fc_revmap, fc_heapBlk, &fc_buf, &fc_off,
										 NULL, BUFFER_LOCK_SHARE, NULL);

		/* 如果范围未摘要，则没有任何操作 */
		if (!fc_brtup)
			break;

		/* 在此语句中第一次执行？ */
		if (fc_bdesc == NULL)
		{
			MemoryContextSwitchTo(fc_indexInfo->ii_Context);
			fc_bdesc = brin_build_desc(fc_idxRel);
			fc_indexInfo->ii_AmCache = (void *) fc_bdesc;
			MemoryContextSwitchTo(fc_oldcxt);
		}
		/* 在此 brininsert 调用中第一次执行？ */
		if (fc_tupcxt == NULL)
		{
			fc_tupcxt = AllocSetContextCreate(CurrentMemoryContext,
										   "brininsert cxt",
										   ALLOCSET_DEFAULT_SIZES);
			MemoryContextSwitchTo(fc_tupcxt);
		}

		fc_dtup = brin_deform_tuple(fc_bdesc, fc_brtup, NULL);

		fc_need_insert = fc_add_values_to_range(fc_idxRel, fc_bdesc, fc_dtup, fc_values, fc_nulls);

		if (!fc_need_insert)
		{
			/*
			 * 该元组与新值一致，因此没有任何操作。
			 */
			LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
		}
		else
		{
			Page		fc_page = BufferGetPage(fc_buf);
			ItemId		fc_lp = PageGetItemId(fc_page, fc_off);
			Size		fc_origsz;
			BrinTuple  *fc_origtup;
			Size		fc_newsz;
			BrinTuple  *fc_newtup;
			bool		fc_samepage;

			/*
			 * 复制旧元组，以便在重新获取锁后进行比较。
			 */
			fc_origsz = ItemIdGetLength(fc_lp);
			fc_origtup = brin_copy_tuple(fc_brtup, fc_origsz, NULL, NULL);

			/*
			 * 在释放锁之前，检查我们是否可以尝试进行同页面
			 * 更新。不过，另一个进程可能会同时在同一页面插入一个元组，
			 * 所以在后续操作中我们必须做好准备，如果这结果不可能的话。
			 */
			fc_newtup = brin_form_tuple(fc_bdesc, fc_heapBlk, fc_dtup, &fc_newsz);
			fc_samepage = brin_can_do_samepage_update(fc_buf, fc_origsz, fc_newsz);
			LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);

			/*
			 * 尝试更新元组。如果出于某种原因这不起作用，
			 * 我们需要从头开始；revmap 现在可能指向此块的不同元组，
			 * 因此我们需要重新计算以确保我们的新堆元组以及其他
			 * 插入者的元组都包含在组合元组中。可能我们根本不需要更新。
			 */
			if (!brin_doupdate(fc_idxRel, fc_pagesPerRange, fc_revmap, fc_heapBlk,
							   fc_buf, fc_off, fc_origtup, fc_origsz, fc_newtup, fc_newsz,
							   fc_samepage))
			{
				/* 不成功；重新开始 */
				MemoryContextResetAndDeleteChildren(fc_tupcxt);
				continue;
			}
		}

		/* 成功！ */
		break;
	}

	brinRevmapTerminate(fc_revmap);
	if (BufferIsValid(fc_buf))
		ReleaseBuffer(fc_buf);
	MemoryContextSwitchTo(fc_oldcxt);
	if (fc_tupcxt != NULL)
		MemoryContextDelete(fc_tupcxt);

	return false;
}

/*
 * 初始化 BRIN 索引扫描的状态。
 *
 * 我们在这里读取元数据页面以确定此
 * 索引建立时的每范围页数。请注意，由于在我们
 * 持有索引锁时无法更改它，因此在 brinrescan
 * 期间不必重新计算它。
 */
IndexScanDesc brinbeginscan(Relation fc_r, int fc_nkeys, int fc_norderbys)
{
	IndexScanDesc fc_scan;
	BrinOpaque *fc_opaque;

	fc_scan = RelationGetIndexScan(fc_r, fc_nkeys, fc_norderbys);

	fc_opaque = (BrinOpaque *) palloc(sizeof(BrinOpaque));
	fc_opaque->bo_rmAccess = brinRevmapInitialize(fc_r, &fc_opaque->bo_pagesPerRange,
											   fc_scan->xs_snapshot);
	fc_opaque->bo_bdesc = brin_build_desc(fc_r);
	fc_scan->opaque = fc_opaque;

	return fc_scan;
}

/*
 * 执行索引扫描。
 *
 * 这个过程是通过从 revmap 读取索引 TID，获取指向它们的索引元组；索引元组中的摘要值与扫描键进行比较。我们将所有与扫描键匹配的索引元组所对应的范围中的页面返回到 TID 位图中。
 *
 * 如果从 revmap 读取的 TID 被视为 InvalidTID，我们知道该范围未被摘要。该范围中的页面需要被返回，无论扫描键是什么。
 */
int64 bringetbitmap(IndexScanDesc fc_scan, TIDBitmap *fc_tbm)
{
	Relation	fc_idxRel = fc_scan->indexRelation;
	Buffer		fc_buf = InvalidBuffer;
	BrinDesc   *fc_bdesc;
	Oid			fc_heapOid;
	Relation	fc_heapRel;
	BrinOpaque *fc_opaque;
	BlockNumber fc_nblocks;
	BlockNumber fc_heapBlk;
	int64		fc_totalpages = 0;
	FmgrInfo   *fc_consistentFn;
	MemoryContext fc_oldcxt;
	MemoryContext fc_perRangeCxt;
	BrinMemTuple *fc_dtup;
	BrinTuple  *fc_btup = NULL;
	Size		fc_btupsz = 0;
	ScanKey   **fc_keys,
			  **fc_nullkeys;
	int		   *fc_nkeys,
			   *fc_nnullkeys;
	int			fc_keyno;
	char	   *fc_ptr;
	Size		fc_len;
	char	   *fc_tmp PG_USED_FOR_ASSERTS_ONLY;

	fc_opaque = (BrinOpaque *) fc_scan->opaque;
	fc_bdesc = fc_opaque->bo_bdesc;
	pgstat_count_index_scan(fc_idxRel);

	/*
	 * 我们需要知道表的大小，以便知道在 revmap 上迭代的长度。
	 */
	fc_heapOid = IndexGetRelation(RelationGetRelid(fc_idxRel), false);
	fc_heapRel = table_open(fc_heapOid, AccessShareLock);
	fc_nblocks = RelationGetNumberOfBlocks(fc_heapRel);
	table_close(fc_heapRel, AccessShareLock);

	/*
	 * 为索引列的一致性支持程序腾出空间。我们在这里不查找它们；我们在第一次看到每个扫描键引用时惰性地进行查找。我们依赖将 fn_oid 清零为 InvalidOid。
	 */
	fc_consistentFn = palloc0(sizeof(FmgrInfo) * fc_bdesc->bd_tupdesc->natts);

	/*
	 * 为我们将传递给一致性支持程序的每个属性的扫描键列表腾出空间。我们不知道哪些属性具有扫描键，因此我们为所有属性分配空间。这可能会使用更多内存，但这可能比确定哪些属性被使用要便宜。
	 *
	 * 我们将 null 键和常规键分开，这样我们就可以轻松地将常规键传递给一致性函数。
	 *
	 * 为了减少分配开销，我们分配一大块内存，然后自己将其切分成更小的数组。所有部分的生命周期完全相同，因此这样做没问题。
	 *
	 * XXX 最宽的索引可以有 32 个属性，因此浪费的内存量微不足道。我们可以发明一种更紧凑的方法（仅为使用的属性保留空间），但这会使匹配变得更加复杂，因此不值得。
	 */
	fc_len =
		MAXALIGN(sizeof(ScanKey *) * fc_bdesc->bd_tupdesc->natts) +	/* 常规键 */
		MAXALIGN(sizeof(ScanKey) * fc_scan->numberOfKeys) * fc_bdesc->bd_tupdesc->natts +
		MAXALIGN(sizeof(int) * fc_bdesc->bd_tupdesc->natts) +
		MAXALIGN(sizeof(ScanKey *) * fc_bdesc->bd_tupdesc->natts) +	/* NULL 键 */
		MAXALIGN(sizeof(ScanKey) * fc_scan->numberOfKeys) * fc_bdesc->bd_tupdesc->natts +
		MAXALIGN(sizeof(int) * fc_bdesc->bd_tupdesc->natts);

	fc_ptr = palloc(fc_len);
	fc_tmp = fc_ptr;

	fc_keys = (ScanKey **) fc_ptr;
	fc_ptr += MAXALIGN(sizeof(ScanKey *) * fc_bdesc->bd_tupdesc->natts);

	fc_nullkeys = (ScanKey **) fc_ptr;
	fc_ptr += MAXALIGN(sizeof(ScanKey *) * fc_bdesc->bd_tupdesc->natts);

	fc_nkeys = (int *) fc_ptr;
	fc_ptr += MAXALIGN(sizeof(int) * fc_bdesc->bd_tupdesc->natts);

	fc_nnullkeys = (int *) fc_ptr;
	fc_ptr += MAXALIGN(sizeof(int) * fc_bdesc->bd_tupdesc->natts);

	for (int fc_i = 0; fc_i < fc_bdesc->bd_tupdesc->natts; fc_i++)
	{
		fc_keys[fc_i] = (ScanKey *) fc_ptr;
		fc_ptr += MAXALIGN(sizeof(ScanKey) * fc_scan->numberOfKeys);

		fc_nullkeys[fc_i] = (ScanKey *) fc_ptr;
		fc_ptr += MAXALIGN(sizeof(ScanKey) * fc_scan->numberOfKeys);
	}

	Assert(fc_tmp + fc_len == fc_ptr);

	/* 将键的数量归零 */
	memset(fc_nkeys, 0, sizeof(int) * fc_bdesc->bd_tupdesc->natts);
	memset(fc_nnullkeys, 0, sizeof(int) * fc_bdesc->bd_tupdesc->natts);

	/* 预处理扫描键 - 将其分割为每个属性数组。 */
	for (fc_keyno = 0; fc_keyno < fc_scan->numberOfKeys; fc_keyno++)
	{
		ScanKey		fc_key = &fc_scan->keyData[fc_keyno];
		AttrNumber	fc_keyattno = fc_key->sk_attno;

		/*
		 * 扫描键的排序规则必须与索引列中使用的排序规则匹配（但只有在搜索不是 IS NULL/ IS NOT NULL 的情况下）。
		 * 否则我们就不应该使用这个索引 ...
		 */
		Assert((fc_key->sk_flags & SK_ISNULL) ||
			   (fc_key->sk_collation ==
				TupleDescAttr(fc_bdesc->bd_tupdesc,
							  fc_keyattno - 1)->attcollation));

		/*
		 * 我们第一次看到这个索引属性，因此根据需要进行初始化。
		 *
		 * 这有点过头 - 我们不知道这个属性有多少扫描键，因此我们只分配可能的最大数量（就好像所有键都是这个属性的）。这可能会浪费一些内存，但我们一般只期望较少的扫描键，因此这应该是微不足道的，并且重复的 repalloc 调用也不是免费的。
		 */
		if (fc_consistentFn[fc_keyattno - 1].fn_oid == InvalidOid)
		{
			FmgrInfo   *fc_tmp;

			/* 我们第一次看到这个属性，因此没有键/NULL 键。 */
			Assert(fc_nkeys[fc_keyattno - 1] == 0);
			Assert(fc_nnullkeys[fc_keyattno - 1] == 0);

			fc_tmp = index_getprocinfo(fc_idxRel, fc_keyattno,
									BRIN_PROCNUM_CONSISTENT);
			fmgr_info_copy(&fc_consistentFn[fc_keyattno - 1], fc_tmp,
						   CurrentMemoryContext);
		}

		/* 将键添加到适当的每属性数组中。 */
		if (fc_key->sk_flags & SK_ISNULL)
		{
			fc_nullkeys[fc_keyattno - 1][fc_nnullkeys[fc_keyattno - 1]] = fc_key;
			fc_nnullkeys[fc_keyattno - 1]++;
		}
		else
		{
			fc_keys[fc_keyattno - 1][fc_nkeys[fc_keyattno - 1]] = fc_key;
			fc_nkeys[fc_keyattno - 1]++;
		}
	}

	/* 从每个范围的内存上下文中分配一个初始的内存中的元组 */
	fc_dtup = brin_new_memtuple(fc_bdesc);

	/*
	 * 设置并使用每个范围的内存上下文，该上下文在我们下面循环时每次重置。这避免了在循环中释放元组的需要。
	 */
	fc_perRangeCxt = AllocSetContextCreate(CurrentMemoryContext,
										"bringetbitmap cxt",
										ALLOCSET_DEFAULT_SIZES);
	fc_oldcxt = MemoryContextSwitchTo(fc_perRangeCxt);

	/*
	 * 现在扫描 revmap。我们从查询堆页面 0 开始，
	 * 每次按每个范围的页面数量递增；这使我们对表有一个全面的
	 * 视图。
	 */
	for (fc_heapBlk = 0; fc_heapBlk < fc_nblocks; fc_heapBlk += fc_opaque->bo_pagesPerRange)
	{
		bool		fc_addrange;
		bool		fc_gottuple = false;
		BrinTuple  *fc_tup;
		OffsetNumber fc_off;
		Size		fc_size;

		CHECK_FOR_INTERRUPTS();

		MemoryContextResetAndDeleteChildren(fc_perRangeCxt);

		fc_tup = brinGetTupleForHeapBlock(fc_opaque->bo_rmAccess, fc_heapBlk, &fc_buf,
									   &fc_off, &fc_size, BUFFER_LOCK_SHARE,
									   fc_scan->xs_snapshot);
		if (fc_tup)
		{
			fc_gottuple = true;
			fc_btup = brin_copy_tuple(fc_tup, fc_size, fc_btup, &fc_btupsz);
			LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
		}

		/*
		 * 对于没有索引元组的页面范围，我们必须返回整个
		 * 范围；否则，将其与扫描键进行比较。
		 */
		if (!fc_gottuple)
		{
			fc_addrange = true;
		}
		else
		{
			fc_dtup = brin_deform_tuple(fc_bdesc, fc_btup, fc_dtup);
			if (fc_dtup->bt_placeholder)
			{
				/*
				 * 占位符元组始终会被返回，无论存储的值
				 * 是什么。
				 */
				fc_addrange = true;
			}
			else
			{
				int			fc_attno;

				/*
				 * 将扫描键与范围内存储的摘要值进行比较。
				 * 如果扫描键匹配，则必须将页面范围添加到
				 * 位图中。我们最初假设该范围需要被
				 * 添加；尤其是在没有键的情况下，这样处理。
				 */
				fc_addrange = true;
				for (fc_attno = 1; fc_attno <= fc_bdesc->bd_tupdesc->natts; fc_attno++)
				{
					BrinValues *fc_bval;
					Datum		fc_add;
					Oid			fc_collation;

					/*
					 * 跳过没有任何扫描键的属性（常规和
					 * IS [NOT] NULL）
					 */
					if (fc_nkeys[fc_attno - 1] == 0 && fc_nnullkeys[fc_attno - 1] == 0)
						continue;

					fc_bval = &fc_dtup->bt_columns[fc_attno - 1];

					/*
					 * 如果 BRIN 元组指示该范围为空，
					 * 我们可以跳过它：没有什么可匹配的。我们不
					 * 需要检查下一个列。
					 */
					if (fc_dtup->bt_empty_range)
					{
						fc_addrange = false;
						break;
					}

					/*
					 * 首先检查是否有任意 IS [NOT] NULL 扫描键，
					 * 以及我们是否违反了它们。在这种情况下，我们可以
					 * 提前终止，而无需调用支持函数。
					 *
					 * 由于可能还有更多键，我们只能在这个循环内
					 * 确定不匹配。
					 */
					if (fc_bdesc->bd_info[fc_attno - 1]->oi_regular_nulls &&
						!fc_check_null_keys(fc_bval, fc_nullkeys[fc_attno - 1],
										 fc_nnullkeys[fc_attno - 1]))
					{
						/*
						 * 如果任何 IS [NOT] NULL 键失败，则整个页面
						 * 范围不能通过。所以终止循环。
						 */
						fc_addrange = false;
						break;
					}

					/*
					 * 所以要么没有 IS [NOT] NULL 键，要么全部
					 * 通过。如果没有常规扫描键，我们就完成了 -
					 * 页面范围匹配。如果有常规键，但
					 * 页面范围标记为“全为 NULL”，则它不可能
					 * 通过（我们假设运算符是
					 * 严格的）。
					 */

					/* 没有常规扫描键 - 整个页面范围通过。 */
					if (!fc_nkeys[fc_attno - 1])
						continue;

					Assert((fc_nkeys[fc_attno - 1] > 0) &&
						   (fc_nkeys[fc_attno - 1] <= fc_scan->numberOfKeys));

					/* 如果全为 NULL，则不可能是一致的。 */
					if (fc_bval->bv_allnulls)
					{
						fc_addrange = false;
						break;
					}

					/*
					 * 来自第一个键的排序（必须对
					 * 同一属性的所有键相同）。
					 */
					fc_collation = fc_keys[fc_attno - 1][0]->sk_collation;

					/*
					 * 检查扫描键是否与页面
					 * 范围值一致；如果一致，则将该范围的页面
					 * 添加到输出位图中。
					 *
					 * 操作类可能支持或不支持处理
					 * 多个扫描键。我们可以根据参数的数量确定这一点 -
					 * 具有额外参数（扫描键数量）的函数确实支持这一点，
					 * 否则我们只需逐个传递扫描键。
					 */
					if (fc_consistentFn[fc_attno - 1].fn_nargs >= 4)
					{
						/* 一次检查所有键 */
						fc_add = FunctionCall4Coll(&fc_consistentFn[fc_attno - 1],
												fc_collation,
												PointerGetDatum(fc_bdesc),
												PointerGetDatum(fc_bval),
												PointerGetDatum(fc_keys[fc_attno - 1]),
												Int32GetDatum(fc_nkeys[fc_attno - 1]));
						fc_addrange = DatumGetBool(fc_add);
					}
					else
					{
						/*
						 * 逐个检查键
						 *
						 * 当有多个扫描键时，未满足
						 * 其中任何一个的标准就足以
						 * 将整个范围丢弃，因此在获取到一个错误的返回值后
						 * 尽快跳出循环。
						 */
						int			fc_keyno;

						for (fc_keyno = 0; fc_keyno < fc_nkeys[fc_attno - 1]; fc_keyno++)
						{
							fc_add = FunctionCall3Coll(&fc_consistentFn[fc_attno - 1],
													fc_keys[fc_attno - 1][fc_keyno]->sk_collation,
													PointerGetDatum(fc_bdesc),
													PointerGetDatum(fc_bval),
													PointerGetDatum(fc_keys[fc_attno - 1][fc_keyno]));
							fc_addrange = DatumGetBool(fc_add);
							if (!fc_addrange)
								break;
						}
					}

					/*
					 * 如果我们发现了一个扫描键消除了该范围，则无需
					 * 检查其他的。
					 */
					if (!fc_addrange)
						break;
				}
			}
		}

		/* 如有需要，将范围内的页面添加到输出位图中 */
		if (fc_addrange)
		{
			BlockNumber fc_pageno;

			for (fc_pageno = fc_heapBlk;
				 fc_pageno <= Min(fc_nblocks, fc_heapBlk + fc_opaque->bo_pagesPerRange) - 1;
				 fc_pageno++)
			{
				MemoryContextSwitchTo(fc_oldcxt);
				tbm_add_page(fc_tbm, fc_pageno);
				fc_totalpages++;
				MemoryContextSwitchTo(fc_perRangeCxt);
			}
		}
	}

	MemoryContextSwitchTo(fc_oldcxt);
	MemoryContextDelete(fc_perRangeCxt);

	if (fc_buf != InvalidBuffer)
		ReleaseBuffer(fc_buf);

	/*
	 * XXX 我们对扫描返回的*页*数量有一个近似值，但对涉及的堆元组数量没有精确的了解。
	 */
	return fc_totalpages * 10;
}

/*
 * 为BRIN索引扫描重新初始化状态
 */
void brinrescan(IndexScanDesc fc_scan, ScanKey fc_scankey, int fc_nscankeys,
		   ScanKey fc_orderbys, int fc_norderbys)
{
	/*
	 * 其他索引访问方法在此时或扫描早期对扫描键进行预处理；这使它们能够通过删除冗余键或在无法满足时提前返回来进行优化；参见_bt_preprocess_keys的例子。将来也可以在这里添加类似的功能。
	 */

	if (fc_scankey && fc_scan->numberOfKeys > 0)
		memmove(fc_scan->keyData, fc_scankey,
				fc_scan->numberOfKeys * sizeof(ScanKeyData));
}

/*
 * 关闭BRIN索引扫描
 */
void brinendscan(IndexScanDesc fc_scan)
{
	BrinOpaque *fc_opaque = (BrinOpaque *) fc_scan->opaque;

	brinRevmapTerminate(fc_opaque->bo_rmAccess);
	brin_free_desc(fc_opaque->bo_bdesc);
	pfree(fc_opaque);
}

/*
 * 针对table_index_build_scan的每个堆元组的回调。
 *
 * 请注意，我们在此处不考虑表末尾的页面范围；它在最后一次调用后包含在构建状态结构中，但未插入到索引中。如有必要，调用者必须确保进行插入。
 */
static void fc_brinbuildCallback(Relation fc_index,
				  ItemPointer fc_tid,
				  Datum *fc_values,
				  bool *fc_isnull,
				  bool fc_tupleIsAlive,
				  void *fc_brstate)
{
	BrinBuildState *fc_state = (BrinBuildState *) fc_brstate;
	BlockNumber fc_thisblock;

	fc_thisblock = ItemPointerGetBlockNumber(fc_tid);

	/*
	 * 如果我们处于属于未来范围的块中，总结我们拥有的内容并重新开始。注意，如果跳过了许多没有活动元组的页面，扫描也可能会这样做；确保为那些页面插入索引元组。
	 */
	while (fc_thisblock > fc_state->bs_currRangeStart + fc_state->bs_pagesPerRange - 1)
	{

		BRIN_elog((DEBUG2,
				   "brinbuildCallback: completed a range: %u--%u",
				   state->bs_currRangeStart,
				   state->bs_currRangeStart + state->bs_pagesPerRange));

		/* 创建索引元组并插入它 */
		fc_form_and_insert_tuple(fc_state);

		/* 将状态设置为对应于下一个范围 */
		fc_state->bs_currRangeStart += fc_state->bs_pagesPerRange;

		/* 为其重新初始化状态 */
		brin_memtuple_initialize(fc_state->bs_dtuple, fc_state->bs_bdesc);
	}

	/* 将当前元组累积到运行状态中 */
	(void) fc_add_values_to_range(fc_index, fc_state->bs_bdesc, fc_state->bs_dtuple,
							   fc_values, fc_isnull);
}

/*
 * brinbuild() -- 构建新的BRIN索引。
 */
IndexBuildResult * brinbuild(Relation fc_heap, Relation fc_index, IndexInfo *fc_indexInfo)
{
	IndexBuildResult *fc_result;
	double		fc_reltuples;
	double		fc_idxtuples;
	BrinRevmap *fc_revmap;
	BrinBuildState *fc_state;
	Buffer		fc_meta;
	BlockNumber fc_pagesPerRange;

	/*
	 * 我们期望每个索引关系被调用一次。
	 */
	if (RelationGetNumberOfBlocks(fc_index) != 0)
		elog(ERROR, "index \"%s\" already contains data",
			 RelationGetRelationName(fc_index));

	/*
	 * 由于在错误时会回滚整个关系的创建，因此不需要临界区。
	 */

	fc_meta = ReadBuffer(fc_index, P_NEW);
	Assert(BufferGetBlockNumber(fc_meta) == BRIN_METAPAGE_BLKNO);
	LockBuffer(fc_meta, BUFFER_LOCK_EXCLUSIVE);

	brin_metapage_init(BufferGetPage(fc_meta), BrinGetPagesPerRange(fc_index),
					   BRIN_CURRENT_VERSION);
	MarkBufferDirty(fc_meta);

	if (RelationNeedsWAL(fc_index))
	{
		xl_brin_createidx fc_xlrec;
		XLogRecPtr	fc_recptr;
		Page		fc_page;

		fc_xlrec.version = BRIN_CURRENT_VERSION;
		fc_xlrec.pagesPerRange = BrinGetPagesPerRange(fc_index);

		XLogBeginInsert();
		XLogRegisterData((char *) &fc_xlrec, SizeOfBrinCreateIdx);
		XLogRegisterBuffer(0, fc_meta, REGBUF_WILL_INIT | REGBUF_STANDARD);

		fc_recptr = XLogInsert(RM_BRIN_ID, XLOG_BRIN_CREATE_INDEX);

		fc_page = BufferGetPage(fc_meta);
		PageSetLSN(fc_page, fc_recptr);
	}

	UnlockReleaseBuffer(fc_meta);

	/*
	 * 初始化我们的状态，包括变形元组状态。
	 */
	fc_revmap = brinRevmapInitialize(fc_index, &fc_pagesPerRange, NULL);
	fc_state = fc_initialize_brin_buildstate(fc_index, fc_revmap, fc_pagesPerRange);

	/*
	 * 现在扫描关系。由于我们希望堆块按物理顺序排列，因此不允许同步扫描。
	 */
	fc_reltuples = table_index_build_scan(fc_heap, fc_index, fc_indexInfo, false, true,
									   fc_brinbuildCallback, (void *) fc_state, NULL);

	/* 处理最终批次 */
	fc_form_and_insert_tuple(fc_state);

	/* 释放资源 */
	fc_idxtuples = fc_state->bs_numtuples;
	brinRevmapTerminate(fc_state->bs_rmAccess);
	fc_terminate_brin_buildstate(fc_state);

	/*
	 * 返回统计信息
	 */
	fc_result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult));

	fc_result->heap_tuples = fc_reltuples;
	fc_result->index_tuples = fc_idxtuples;

	return fc_result;
}

void brinbuildempty(Relation fc_index)
{
	Buffer		fc_metabuf;

	/* 一个空的BRIN索引只有一个元数据页面。 */
	fc_metabuf =
		ReadBufferExtended(fc_index, INIT_FORKNUM, P_NEW, RBM_NORMAL, NULL);
	LockBuffer(fc_metabuf, BUFFER_LOCK_EXCLUSIVE);

	/* 初始化和xlog元数据缓冲区。 */
	START_CRIT_SECTION();
	brin_metapage_init(BufferGetPage(fc_metabuf), BrinGetPagesPerRange(fc_index),
					   BRIN_CURRENT_VERSION);
	MarkBufferDirty(fc_metabuf);
	log_newpage_buffer(fc_metabuf, true);
	END_CRIT_SECTION();

	UnlockReleaseBuffer(fc_metabuf);
}

/*
 * brinbulkdelete
 *		由于BRIN索引中没有每个堆元组的索引元组，因此我们能做的并不多。
 *
 * XXX 我们可以将项目元组标记为“脏”（当删除最小或最大堆元组时），这意味着需要重新运行受影响范围的摘要。需要在brintuples中添加一个额外的标志来实现这一点。
 */
IndexBulkDeleteResult * brinbulkdelete(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats,
			   IndexBulkDeleteCallback fc_callback, void *fc_callback_state)
{
	/* 如果第一次遍历则分配统计信息，否则重用现有结构 */
	if (fc_stats == NULL)
		fc_stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));

	return fc_stats;
}

/*
 * 此例程负责“清理”BRIN索引：我们只对当前未摘要的范围进行摘要。
 */
IndexBulkDeleteResult * brinvacuumcleanup(IndexVacuumInfo *fc_info, IndexBulkDeleteResult *fc_stats)
{
	Relation	fc_heapRel;

	/* 在ANALYZE ONLY模式下无操作 */
	if (fc_info->analyze_only)
		return fc_stats;

	if (!fc_stats)
		fc_stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult));
	fc_stats->num_pages = RelationGetNumberOfBlocks(fc_info->index);
	/* 其他统计信息通过归零进行初始化 */

	fc_heapRel = table_open(IndexGetRelation(RelationGetRelid(fc_info->index), false),
						 AccessShareLock);

	fc_brin_vacuum_scan(fc_info->index, fc_info->strategy);

	fc_brinsummarize(fc_info->index, fc_heapRel, BRIN_ALL_BLOCKRANGES, false,
				  &fc_stats->num_index_tuples, &fc_stats->num_index_tuples);

	table_close(fc_heapRel, AccessShareLock);

	return fc_stats;
}

/*
 * BRIN索引的reloptions处理器
 */
bytea * brinoptions(Datum fc_reloptions, bool fc_validate)
{
	static const relopt_parse_elt fc_tab[] = {
		{"pages_per_range", RELOPT_TYPE_INT, offsetof(BrinOptions, pagesPerRange)},
		{"autosummarize", RELOPT_TYPE_BOOL, offsetof(BrinOptions, autosummarize)}
	};

	return (bytea *) build_reloptions(fc_reloptions, fc_validate,
									  RELOPT_KIND_BRIN,
									  sizeof(BrinOptions),
									  fc_tab, lengthof(fc_tab));
}

/*
 * 可通过SQL调用的函数，用于扫描索引并总结所有当前未总结的范围。
 */
Datum brin_summarize_new_values(PG_FUNCTION_ARGS)
{
	Datum		fc_relation = PG_GETARG_DATUM(0);

	return DirectFunctionCall2(brin_summarize_range,
							   fc_relation,
							   Int64GetDatum((int64) BRIN_ALL_BLOCKRANGES));
}

/*
 * SQL可调用函数，用于总结指定的页面范围，如果尚未
 * 总结。如果第二个参数为 BRIN_ALL_BLOCKRANGES，则总结所有
 * 未总结的范围。
 */
Datum brin_summarize_range(PG_FUNCTION_ARGS)
{
	Oid			fc_indexoid = PG_GETARG_OID(0);
	int64		fc_heapBlk64 = PG_GETARG_INT64(1);
	BlockNumber fc_heapBlk;
	Oid			fc_heapoid;
	Relation	fc_indexRel;
	Relation	fc_heapRel;
	Oid			fc_save_userid;
	int			fc_save_sec_context;
	int			fc_save_nestlevel;
	double		fc_numSummarized = 0;

	if (RecoveryInProgress())
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("recovery is in progress"),
				 errhint("BRIN control functions cannot be executed during recovery.")));

	if (fc_heapBlk64 > BRIN_ALL_BLOCKRANGES || fc_heapBlk64 < 0)
		ereport(ERROR,
				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
				 errmsg("block number out of range: %lld",
						(long long) fc_heapBlk64)));
	fc_heapBlk = (BlockNumber) fc_heapBlk64;

	/*
	 * 我们必须在索引之前锁定表，以避免死锁。然而，如果传递的
	 * indexoid 不是一个索引，那么 IndexGetRelation() 将失败。
	 * 与其发出一个不太有帮助的错误消息，不如推迟抱怨，期望
	 * 下面的是否为索引测试会失败。
	 */
	fc_heapoid = IndexGetRelation(fc_indexoid, true);
	if (OidIsValid(fc_heapoid))
	{
		fc_heapRel = table_open(fc_heapoid, ShareUpdateExclusiveLock);

		/*
		 * 自动真空调用我们。为了它的利益，切换到表所有者的
		 * userid，以便任何索引函数作为该用户运行。还要
		 * 锁定安全受限操作，并安排将 GUC 变量更改本地
		 * 于此命令。当从 SQL 调用时这没有害处，尽管
		 * 不必要，因为如果用户不拥有索引，我们很快就会失败。
		 */
		GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);
		SetUserIdAndSecContext(fc_heapRel->rd_rel->relowner,
							   fc_save_sec_context | SECURITY_RESTRICTED_OPERATION);
		fc_save_nestlevel = NewGUCNestLevel();
	}
	else
	{
		fc_heapRel = NULL;
		/* 只需设置这些以抑制“未初始化变量”警告 */
		fc_save_userid = InvalidOid;
		fc_save_sec_context = -1;
		fc_save_nestlevel = -1;
	}

	fc_indexRel = index_open(fc_indexoid, ShareUpdateExclusiveLock);

	/* 必须是 BRIN 索引 */
	if (fc_indexRel->rd_rel->relkind != RELKIND_INDEX ||
		fc_indexRel->rd_rel->relam != BRIN_AM_OID)
		ereport(ERROR,
				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
				 errmsg("\"%s\" is not a BRIN index",
						RelationGetRelationName(fc_indexRel))));

	/* 用户必须拥有索引（与 VACUUM 所需的权限相当） */
	if (fc_heapRel != NULL && !pg_class_ownercheck(fc_indexoid, fc_save_userid))
		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_INDEX,
					   RelationGetRelationName(fc_indexRel));

	/*
	 * 由于我们在没有任何锁的情况下进行了 IndexGetRelation 调用，
	 * 因此有可能与索引的删除/重建发生竞态，导致我们获得了错误的表。
	 * 重新检查。
	 */
	if (fc_heapRel == NULL || fc_heapoid != IndexGetRelation(fc_indexoid, false))
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_TABLE),
				 errmsg("could not open parent table of index \"%s\"",
						RelationGetRelationName(fc_indexRel))));

	/* 参见 gin_clean_pending_list() */
	if (fc_indexRel->rd_index->indisvalid)
		fc_brinsummarize(fc_indexRel, fc_heapRel, fc_heapBlk, true, &fc_numSummarized, NULL);
	else
		ereport(DEBUG1,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("index \"%s\" is not valid",
						RelationGetRelationName(fc_indexRel))));

	/* 回滚索引函数执行的任何 GUC 更改 */
	AtEOXact_GUC(false, fc_save_nestlevel);

	/* 恢复 userid 和安全上下文 */
	SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);

	relation_close(fc_indexRel, ShareUpdateExclusiveLock);
	relation_close(fc_heapRel, ShareUpdateExclusiveLock);

	PG_RETURN_INT32((int32) fc_numSummarized);
}

/*
 * SQL可调用接口，用于标记范围不再被总结
 */
Datum brin_desummarize_range(PG_FUNCTION_ARGS)
{
	Oid			fc_indexoid = PG_GETARG_OID(0);
	int64		fc_heapBlk64 = PG_GETARG_INT64(1);
	BlockNumber fc_heapBlk;
	Oid			fc_heapoid;
	Relation	fc_heapRel;
	Relation	fc_indexRel;
	bool		fc_done;

	if (RecoveryInProgress())
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("recovery is in progress"),
				 errhint("BRIN control functions cannot be executed during recovery.")));

	if (fc_heapBlk64 > MaxBlockNumber || fc_heapBlk64 < 0)
		ereport(ERROR,
				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
				 errmsg("block number out of range: %lld",
						(long long) fc_heapBlk64)));
	fc_heapBlk = (BlockNumber) fc_heapBlk64;

	/*
	 * 我们必须在索引之前锁定表，以避免死锁。然而，如果传递的
	 * indexoid 不是一个索引，那么 IndexGetRelation() 将失败。
	 * 与其发出一个不太有帮助的错误消息，不如推迟抱怨，期望
	 * 下面的是否为索引测试会失败。
	 *
	 * 与 brin_summarize_range() 不同，自动真空从不调用此方法。
	 * 因此，我们不切换 userid。
	 */
	fc_heapoid = IndexGetRelation(fc_indexoid, true);
	if (OidIsValid(fc_heapoid))
		fc_heapRel = table_open(fc_heapoid, ShareUpdateExclusiveLock);
	else
		fc_heapRel = NULL;

	fc_indexRel = index_open(fc_indexoid, ShareUpdateExclusiveLock);

	/* 必须是 BRIN 索引 */
	if (fc_indexRel->rd_rel->relkind != RELKIND_INDEX ||
		fc_indexRel->rd_rel->relam != BRIN_AM_OID)
		ereport(ERROR,
				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
				 errmsg("\"%s\" is not a BRIN index",
						RelationGetRelationName(fc_indexRel))));

	/* 用户必须拥有索引（与 VACUUM 所需的权限相当） */
	if (!pg_class_ownercheck(fc_indexoid, GetUserId()))
		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_INDEX,
					   RelationGetRelationName(fc_indexRel));

	/*
	 * 由于我们在没有任何锁的情况下进行了 IndexGetRelation 调用，
	 * 因此有可能与索引的删除/重建发生竞态，导致我们获得了错误的表。
	 * 重新检查。
	 */
	if (fc_heapRel == NULL || fc_heapoid != IndexGetRelation(fc_indexoid, false))
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_TABLE),
				 errmsg("could not open parent table of index \"%s\"",
						RelationGetRelationName(fc_indexRel))));

	/* 参见 gin_clean_pending_list() */
	if (fc_indexRel->rd_index->indisvalid)
	{
		/* revmap 执行繁重工作 */
		do
		{
			fc_done = brinRevmapDesummarizeRange(fc_indexRel, fc_heapBlk);
		}
		while (!fc_done);
	}
	else
		ereport(DEBUG1,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("index \"%s\" is not valid",
						RelationGetRelationName(fc_indexRel))));

	relation_close(fc_indexRel, ShareUpdateExclusiveLock);
	relation_close(fc_heapRel, ShareUpdateExclusiveLock);

	PG_RETURN_VOID();
}

/*
 * 构建用于创建或扫描 BRIN 索引的 BrinDesc
 */
BrinDesc *
brin_build_desc(Relation fc_rel)
{
	BrinOpcInfo **fc_opcinfo;
	BrinDesc   *fc_bdesc;
	TupleDesc	fc_tupdesc;
	int			fc_totalstored = 0;
	int			fc_keyno;
	long		fc_totalsize;
	MemoryContext fc_cxt;
	MemoryContext fc_oldcxt;

	fc_cxt = AllocSetContextCreate(CurrentMemoryContext,
								"brin desc cxt",
								ALLOCSET_SMALL_SIZES);
	fc_oldcxt = MemoryContextSwitchTo(fc_cxt);
	fc_tupdesc = RelationGetDescr(fc_rel);

	/*
	 * 获取每个索引列的 BrinOpcInfo。在此期间，累积
	 * 存储的列数，因为该数量是由操作类别定义的。
	 */
	fc_opcinfo = (BrinOpcInfo **) palloc(sizeof(BrinOpcInfo *) * fc_tupdesc->natts);
	for (fc_keyno = 0; fc_keyno < fc_tupdesc->natts; fc_keyno++)
	{
		FmgrInfo   *fc_opcInfoFn;
		Form_pg_attribute fc_attr = TupleDescAttr(fc_tupdesc, fc_keyno);

		fc_opcInfoFn = index_getprocinfo(fc_rel, fc_keyno + 1, BRIN_PROCNUM_OPCINFO);

		fc_opcinfo[fc_keyno] = (BrinOpcInfo *)
			DatumGetPointer(FunctionCall1(fc_opcInfoFn, fc_attr->atttypid));
		fc_totalstored += fc_opcinfo[fc_keyno]->oi_nstored;
	}

	/* 分配我们的结果结构并填写它 */
	fc_totalsize = offsetof(BrinDesc, bd_info) +
		sizeof(BrinOpcInfo *) * fc_tupdesc->natts;

	fc_bdesc = palloc(fc_totalsize);
	fc_bdesc->bd_context = fc_cxt;
	fc_bdesc->bd_index = fc_rel;
	fc_bdesc->bd_tupdesc = fc_tupdesc;
	fc_bdesc->bd_disktdesc = NULL; /* 懒惰生成 */
	fc_bdesc->bd_totalstored = fc_totalstored;

	for (fc_keyno = 0; fc_keyno < fc_tupdesc->natts; fc_keyno++)
		fc_bdesc->bd_info[fc_keyno] = fc_opcinfo[fc_keyno];
	pfree(fc_opcinfo);

	MemoryContextSwitchTo(fc_oldcxt);

	return fc_bdesc;
}

void brin_free_desc(BrinDesc *fc_bdesc)
{
	/* 确保 tupdesc 仍然有效 */
	Assert(fc_bdesc->bd_tupdesc->tdrefcount >= 1);
	/* 无需逐个释放 pfree */
	MemoryContextDelete(fc_bdesc->bd_context);
}

/*
 * 将索引的统计数据提取到 *stats
 */
void brinGetStats(Relation fc_index, BrinStatsData *fc_stats)
{
	Buffer		fc_metabuffer;
	Page		fc_metapage;
	BrinMetaPageData *fc_metadata;

	fc_metabuffer = ReadBuffer(fc_index, BRIN_METAPAGE_BLKNO);
	LockBuffer(fc_metabuffer, BUFFER_LOCK_SHARE);
	fc_metapage = BufferGetPage(fc_metabuffer);
	fc_metadata = (BrinMetaPageData *) PageGetContents(fc_metapage);

	fc_stats->pagesPerRange = fc_metadata->pagesPerRange;
	fc_stats->revmapNumPages = fc_metadata->lastRevmapPage - 1;

	UnlockReleaseBuffer(fc_metabuffer);
}

/*
 * 初始化适用于在给定索引上创建元组的 BrinBuildState。
 */
static BrinBuildState *
fc_initialize_brin_buildstate(Relation fc_idxRel, BrinRevmap *fc_revmap,
						   BlockNumber fc_pagesPerRange)
{
	BrinBuildState *fc_state;

	fc_state = palloc(sizeof(BrinBuildState));

	fc_state->bs_irel = fc_idxRel;
	fc_state->bs_numtuples = 0;
	fc_state->bs_currentInsertBuf = InvalidBuffer;
	fc_state->bs_pagesPerRange = fc_pagesPerRange;
	fc_state->bs_currRangeStart = 0;
	fc_state->bs_rmAccess = fc_revmap;
	fc_state->bs_bdesc = brin_build_desc(fc_idxRel);
	fc_state->bs_dtuple = brin_new_memtuple(fc_state->bs_bdesc);

	return fc_state;
}

/*
 * 释放与 BrinBuildState 相关的资源。
 */
static void fc_terminate_brin_buildstate(BrinBuildState *fc_state)
{
	/*
	 * 释放最后一个使用的索引缓冲区。我们还可以确保
	 * 在该页面中剩余的任何空闲空间在 FSM 中也是可用的。
	 */
	if (!BufferIsInvalid(fc_state->bs_currentInsertBuf))
	{
		Page		fc_page;
		Size		fc_freespace;
		BlockNumber fc_blk;

		fc_page = BufferGetPage(fc_state->bs_currentInsertBuf);
		fc_freespace = PageGetFreeSpace(fc_page);
		fc_blk = BufferGetBlockNumber(fc_state->bs_currentInsertBuf);
		ReleaseBuffer(fc_state->bs_currentInsertBuf);
		RecordPageWithFreeSpace(fc_state->bs_irel, fc_blk, fc_freespace);
		FreeSpaceMapVacuumRange(fc_state->bs_irel, fc_blk, fc_blk + 1);
	}

	brin_free_desc(fc_state->bs_bdesc);
	pfree(fc_state->bs_dtuple);
	pfree(fc_state);
}

/*
 * 在给定的 BRIN 索引上，汇总与给定的堆块编号
 * 相对应的堆页面范围。
 *
 * 该例程可以与堆中的插入并行运行。为了避免
 * 从摘要元组中遗漏这些值，我们首先将占位符
 * 索引元组插入索引中，然后执行堆扫描；与扫描并发的事务
 * 更新占位符元组。扫描后，我们将占位符元组与该例程计算的
 * 元组进行合并。索引值的更新是在一个循环中进行的，因此
 * 如果在我们读取占位符元组后有人更新了它，我们会检测到这种情况并重试。
 * 这确保并发插入的元组不会丢失。
 *
 * 另一个极端情况是请求该例程汇总表尾的
 * 部分范围。heapNumBlocks 是（可能过时的）
 * 表大小；如果我们注意到请求的范围超过了该大小，
 * 我们在插入占位符元组后重新计算表大小，以
 * 避免遗漏最近附加的页面。
 */
static void fc_summarize_range(IndexInfo *fc_indexInfo, BrinBuildState *fc_state, Relation fc_heapRel,
				BlockNumber fc_heapBlk, BlockNumber fc_heapNumBlks)
{
	Buffer		fc_phbuf;
	BrinTuple  *fc_phtup;
	Size		fc_phsz;
	OffsetNumber fc_offset;
	BlockNumber fc_scanNumBlks;

	/*
	 * 插入占位符元组
	 */
	fc_phbuf = InvalidBuffer;
	fc_phtup = brin_form_placeholder_tuple(fc_state->bs_bdesc, fc_heapBlk, &fc_phsz);
	fc_offset = brin_doinsert(fc_state->bs_irel, fc_state->bs_pagesPerRange,
						   fc_state->bs_rmAccess, &fc_phbuf,
						   fc_heapBlk, fc_phtup, fc_phsz);

	/*
	 * 计算范围结束。我们对表持有 ShareUpdateExclusive 锁，因此它
	 * 不能并发收缩（但可以增长）。
	 */
	Assert(fc_heapBlk % fc_state->bs_pagesPerRange == 0);
	if (fc_heapBlk + fc_state->bs_pagesPerRange > fc_heapNumBlks)
	{
		/*
		 * 如果我们被要求扫描我们认为是表上最后一个范围的内容
		 * （即可能是部分范围），我们需要在插入占位符
		 * 元组后重新计算我们对最新页面的理解。
		 * 任何稍后扩展表的操作都会更新占位符元组，
		 * 因此我们自己不扫描这些页面也没关系。
		 * 小心：表可能已经扩展到当前范围之外，因此限制我们的结果。
		 *
		 * 幸运的是，这种情况发生的频率应该很低。
		 */
		fc_scanNumBlks = Min(RelationGetNumberOfBlocks(fc_heapRel) - fc_heapBlk,
						  fc_state->bs_pagesPerRange);
	}
	else
	{
		/* 简单情况：范围已知为完整 */
		fc_scanNumBlks = fc_state->bs_pagesPerRange;
	}

	/*
	 * 执行覆盖指定页面范围内堆块的部分堆扫描，
	 * 汇总其中的堆元组。该扫描在 brinbuildCallback
	 * 创建新索引条目之前停止。
	 *
	 * 请注意，使用“任何可见”的模式对
	 * table_index_build_range_scan 是至关重要的：否则，
	 * 我们会错过仍在进行中的事务插入的元组，和其他一些边缘情况。
	 */
	fc_state->bs_currRangeStart = fc_heapBlk;
	table_index_build_range_scan(fc_heapRel, fc_state->bs_irel, fc_indexInfo, false, true, false,
								 fc_heapBlk, fc_scanNumBlks,
								 fc_brinbuildCallback, (void *) fc_state, NULL);

	/*
	 * 现在我们用占位符元组更新扫描获取的值。
	 * 我们在一个循环中执行这个操作，只有在能够
	 * 成功更新占位符元组时才终止；如果不能，这意味着
	 * 其他人修改了我们读取后的占位符元组。
	 */
	for (;;)
	{
		BrinTuple  *fc_newtup;
		Size		fc_newsize;
		bool		fc_didupdate;
		bool		fc_samepage;

		CHECK_FOR_INTERRUPTS();

		/*
		 * 更新摘要元组并尝试更新。
		 */
		fc_newtup = brin_form_tuple(fc_state->bs_bdesc,
								 fc_heapBlk, fc_state->bs_dtuple, &fc_newsize);
		fc_samepage = brin_can_do_samepage_update(fc_phbuf, fc_phsz, fc_newsize);
		fc_didupdate =
			brin_doupdate(fc_state->bs_irel, fc_state->bs_pagesPerRange,
						  fc_state->bs_rmAccess, fc_heapBlk, fc_phbuf, fc_offset,
						  fc_phtup, fc_phsz, fc_newtup, fc_newsize, fc_samepage);
		brin_free_tuple(fc_phtup);
		brin_free_tuple(fc_newtup);

		/* 如果更新成功，我们就完成了。 */
		if (fc_didupdate)
			break;

		/*
		 * 如果更新没有成功，可能是因为有其他人同时更新了
		 * 占位符元组。提取新版本，将其与我们从扫描中获得的值
		 * 联合，并重新开始。（更新失败还有其他原因，但处理它们
		 * 是简单的。）
		 */
		fc_phtup = brinGetTupleForHeapBlock(fc_state->bs_rmAccess, fc_heapBlk, &fc_phbuf,
										 &fc_offset, &fc_phsz, BUFFER_LOCK_SHARE,
										 NULL);
		/* 占位符元组必须存在 */
		if (fc_phtup == NULL)
			elog(ERROR, "missing placeholder tuple");
		fc_phtup = brin_copy_tuple(fc_phtup, fc_phsz, NULL, NULL);
		LockBuffer(fc_phbuf, BUFFER_LOCK_UNLOCK);

		/* 将其合并到堆扫描的元组中 */
		fc_union_tuples(fc_state->bs_bdesc, fc_state->bs_dtuple, fc_phtup);
	}

	ReleaseBuffer(fc_phbuf);
}

/*
 * 概述尚未被概述的页面范围。如果 pageRange 是
 * BRIN_ALL_BLOCKRANGES，则整个表将被扫描；否则，仅扫描
 * 包含给定堆页面编号的页面范围。
 * 如果 include_partial 为 true，则会概述表末尾的部分范围，
 * 否则不概述。
 *
 * 对于每个插入的新索引元组，如果 *numSummarized (如果不是 NULL)
 * 增加；对于每个现有元组，*numExisting (如果不是 NULL)
 * 增加。
 */
static void fc_brinsummarize(Relation fc_index, Relation fc_heapRel, BlockNumber fc_pageRange,
			  bool fc_include_partial, double *fc_numSummarized, double *fc_numExisting)
{
	BrinRevmap *fc_revmap;
	BrinBuildState *fc_state = NULL;
	IndexInfo  *fc_indexInfo = NULL;
	BlockNumber fc_heapNumBlocks;
	BlockNumber fc_pagesPerRange;
	Buffer		fc_buf;
	BlockNumber fc_startBlk;

	fc_revmap = brinRevmapInitialize(fc_index, &fc_pagesPerRange, NULL);

	/* 确定要处理的页面范围 */
	fc_heapNumBlocks = RelationGetNumberOfBlocks(fc_heapRel);
	if (fc_pageRange == BRIN_ALL_BLOCKRANGES)
		fc_startBlk = 0;
	else
	{
		fc_startBlk = (fc_pageRange / fc_pagesPerRange) * fc_pagesPerRange;
		fc_heapNumBlocks = Min(fc_heapNumBlocks, fc_startBlk + fc_pagesPerRange);
	}
	if (fc_startBlk > fc_heapNumBlocks)
	{
		/* 如果起始点超出表的结束，则无事可做 */
		brinRevmapTerminate(fc_revmap);
		return;
	}

	/*
	 * 扫描 revmap 以查找未被概述的项。
	 */
	fc_buf = InvalidBuffer;
	for (; fc_startBlk < fc_heapNumBlocks; fc_startBlk += fc_pagesPerRange)
	{
		BrinTuple  *fc_tup;
		OffsetNumber fc_off;

		/*
		 * 除非要求即使部分范围也进行概述，否则如果我们认为下一个
		 * 范围是部分的，就现在离开。调用者在通常在进行大宗数据
		 * 加载完成后（brin_summarize_new_values）时传递 true，
		 * 在通常作为任意调度的维护命令（清理）结果时传递 false。
		 */
		if (!fc_include_partial &&
			(fc_startBlk + fc_pagesPerRange > fc_heapNumBlocks))
			break;

		CHECK_FOR_INTERRUPTS();

		fc_tup = brinGetTupleForHeapBlock(fc_revmap, fc_startBlk, &fc_buf, &fc_off, NULL,
									   BUFFER_LOCK_SHARE, NULL);
		if (fc_tup == NULL)
		{
			/* 此堆范围没有 revmap 条目。对其进行概述。 */
			if (fc_state == NULL)
			{
				/* 第一次通过 */
				Assert(!fc_indexInfo);
				fc_state = fc_initialize_brin_buildstate(fc_index, fc_revmap,
												   fc_pagesPerRange);
				fc_indexInfo = BuildIndexInfo(fc_index);
			}
			fc_summarize_range(fc_indexInfo, fc_state, fc_heapRel, fc_startBlk, fc_heapNumBlocks);

			/* 并重新初始化下一个范围的状态 */
			brin_memtuple_initialize(fc_state->bs_dtuple, fc_state->bs_bdesc);

			if (fc_numSummarized)
				*fc_numSummarized += 1.0;
		}
		else
		{
			if (fc_numExisting)
				*fc_numExisting += 1.0;
			LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);
		}
	}

	if (BufferIsValid(fc_buf))
		ReleaseBuffer(fc_buf);

	/* 释放资源 */
	brinRevmapTerminate(fc_revmap);
	if (fc_state)
	{
		fc_terminate_brin_buildstate(fc_state);
		pfree(fc_indexInfo);
	}
}

/*
 * 在构建状态下，给定一个变形元组，将其转换为磁盘上的
 * 格式并将其插入索引，使 revmap 指向它。
 */
static void fc_form_and_insert_tuple(BrinBuildState *fc_state)
{
	BrinTuple  *fc_tup;
	Size		fc_size;

	fc_tup = brin_form_tuple(fc_state->bs_bdesc, fc_state->bs_currRangeStart,
						  fc_state->bs_dtuple, &fc_size);
	brin_doinsert(fc_state->bs_irel, fc_state->bs_pagesPerRange, fc_state->bs_rmAccess,
				  &fc_state->bs_currentInsertBuf, fc_state->bs_currRangeStart,
				  fc_tup, fc_size);
	fc_state->bs_numtuples++;

	pfree(fc_tup);
}

/*
 * 给定两个变形元组，调整第一个元组，以使其与两个
 * 元组中的概述值一致。
 */
static void fc_union_tuples(BrinDesc *fc_bdesc, BrinMemTuple *fc_a, BrinTuple *fc_b)
{
	int			fc_keyno;
	BrinMemTuple *fc_db;
	MemoryContext fc_cxt;
	MemoryContext fc_oldcxt;

	/* 使用我们自己的内存上下文以避免零售 pfree */
	fc_cxt = AllocSetContextCreate(CurrentMemoryContext,
								"brin union",
								ALLOCSET_DEFAULT_SIZES);
	fc_oldcxt = MemoryContextSwitchTo(fc_cxt);
	fc_db = brin_deform_tuple(fc_bdesc, fc_b, NULL);
	MemoryContextSwitchTo(fc_oldcxt);

	/*
	 * 检查范围是否为空。
	 *
	 * 如果其中至少一个为空，我们根本不需要调用每个键的联合
	 * 函数。如果 "b" 为空，我们只需将 "a" 作为结果（它可能
	 * 也是空的，但没关系）。如果 "a" 为空但 "b" 不为空，我们
	 * 将 "b" 作为结果（但我们必须先将数据复制到 "a" 中）。
	 *
	 * 仅当两个范围都非空时，我们才会真正进行每个键的合并。
	 */

	/* 如果 "b" 为空 - 忽略它，只需使用 "a"（即使它是空等）。 */
	if (fc_db->bt_empty_range)
	{
		/* 跳过每个键的合并 */
		MemoryContextDelete(fc_cxt);
		return;
	}

	/*
	 * 现在我们知道 "b" 不是空的。如果 "a" 为空，则 "b" 是结果。
	 * 但我们需要首先将数据从 "b" 复制到 "a"，因为这就是我们
	 * 返回结果的方式。
	 *
	 * 我们还必须复制所有的全局/每个键标志等。
	 */
	if (fc_a->bt_empty_range)
	{
		for (fc_keyno = 0; fc_keyno < fc_bdesc->bd_tupdesc->natts; fc_keyno++)
		{
			int			fc_i;
			BrinValues *fc_col_a = &fc_a->bt_columns[fc_keyno];
			BrinValues *fc_col_b = &fc_db->bt_columns[fc_keyno];
			BrinOpcInfo *fc_opcinfo = fc_bdesc->bd_info[fc_keyno];

			fc_col_a->bv_allnulls = fc_col_b->bv_allnulls;
			fc_col_a->bv_hasnulls = fc_col_b->bv_hasnulls;

			/* 如果 "b" 没有数据，我们完成了。 */
			if (fc_col_b->bv_allnulls)
				continue;

			for (fc_i = 0; fc_i < fc_opcinfo->oi_nstored; fc_i++)
				fc_col_a->bv_values[fc_i] =
					datumCopy(fc_col_b->bv_values[fc_i],
							  fc_opcinfo->oi_typcache[fc_i]->typbyval,
							  fc_opcinfo->oi_typcache[fc_i]->typlen);
		}

		/* "a" 开始时为空，但 "b" 不为空，所以记住这一点 */
		fc_a->bt_empty_range = false;

		/* 跳过每个键的合并 */
		MemoryContextDelete(fc_cxt);
		return;
	}

	/* 现在我们知道两个范围都不是空的。 */
	for (fc_keyno = 0; fc_keyno < fc_bdesc->bd_tupdesc->natts; fc_keyno++)
	{
		FmgrInfo   *fc_unionFn;
		BrinValues *fc_col_a = &fc_a->bt_columns[fc_keyno];
		BrinValues *fc_col_b = &fc_db->bt_columns[fc_keyno];
		BrinOpcInfo *fc_opcinfo = fc_bdesc->bd_info[fc_keyno];

		if (fc_opcinfo->oi_regular_nulls)
		{
			/* "b" 概述是否表示任何 NULL 值？ */
			bool		fc_b_has_nulls = (fc_col_b->bv_hasnulls || fc_col_b->bv_allnulls);

			/* 调整 "hasnulls". */
			if (!fc_col_a->bv_allnulls && fc_b_has_nulls)
				fc_col_a->bv_hasnulls = true;

			/* 如果 B 中没有值，就没有其他可做的事情。 */
			if (fc_col_b->bv_allnulls)
				continue;

			/*
			 * 调整 "allnulls"。如果 A 没有值，只需将 B 中的值复制到 A 中，我们就完成了。 
			 * 在这种情况下，我们无法运行操作，因为 A 中的值可能包含垃圾。 
			 * 注意我们已经确定 B 中包含值。
			 *
			 * 同样调整 "hasnulls"，以免忘记摘要代表 NULL 值。 
			 * 这与之前的更新不冗余，因为这仅在 allnulls=false 时发生。
			 */
			if (fc_col_a->bv_allnulls)
			{
				int			fc_i;

				fc_col_a->bv_allnulls = false;
				fc_col_a->bv_hasnulls = true;

				for (fc_i = 0; fc_i < fc_opcinfo->oi_nstored; fc_i++)
					fc_col_a->bv_values[fc_i] =
						datumCopy(fc_col_b->bv_values[fc_i],
								  fc_opcinfo->oi_typcache[fc_i]->typbyval,
								  fc_opcinfo->oi_typcache[fc_i]->typlen);

				continue;
			}
		}

		fc_unionFn = index_getprocinfo(fc_bdesc->bd_index, fc_keyno + 1,
									BRIN_PROCNUM_UNION);
		FunctionCall3Coll(fc_unionFn,
						  fc_bdesc->bd_index->rd_indcollation[fc_keyno],
						  PointerGetDatum(fc_bdesc),
						  PointerGetDatum(fc_col_a),
						  PointerGetDatum(fc_col_b));
	}

	MemoryContextDelete(fc_cxt);
}

/*
 * brin_vacuum_scan
 *		在 VACUUM 期间对索引进行完整扫描。
 *
 * 这个例程扫描完整的索引，寻找未编目索引页面，
 * 即那些可能由于崩溃后索引扩展而丢失的页面等。
 */
static void fc_brin_vacuum_scan(Relation fc_idxrel, BufferAccessStrategy fc_strategy)
{
	BlockNumber fc_nblocks;
	BlockNumber fc_blkno;

	/*
	 * 按物理顺序扫描索引，并清理每个页面中的任何可能的混乱。
	 */
	fc_nblocks = RelationGetNumberOfBlocks(fc_idxrel);
	for (fc_blkno = 0; fc_blkno < fc_nblocks; fc_blkno++)
	{
		Buffer		fc_buf;

		CHECK_FOR_INTERRUPTS();

		fc_buf = ReadBufferExtended(fc_idxrel, MAIN_FORKNUM, fc_blkno,
								 RBM_NORMAL, fc_strategy);

		brin_page_cleanup(fc_idxrel, fc_buf);

		ReleaseBuffer(fc_buf);
	}

	/*
	 * 还要更新索引的 FSM 中所有上级页面。 
	 * 这不仅确保我们传播了由 brin_page_cleanup 所做的叶子页面 FSM 更新，
	 * 还确保修复任何预先存在的损坏或过时。
	 */
	FreeSpaceMapVacuum(fc_idxrel);
}

static bool fc_add_values_to_range(Relation fc_idxRel, BrinDesc *fc_bdesc, BrinMemTuple *fc_dtup,
					Datum *fc_values, bool *fc_nulls)
{
	int			fc_keyno;

	/* 如果范围开始为空，我们肯定会修改它。 */
	bool		fc_modified = fc_dtup->bt_empty_range;

	/*
	 * 将新元组的键值与存储的索引值进行比较；如果新元组
	 * 不符合原始范围，我们的变形元组将被更新（注意这意味着我们无法提前
	 * 跳出循环）。请注意这是否发生，以便我们知道稍后插入已修改的元组。
	 */
	for (fc_keyno = 0; fc_keyno < fc_bdesc->bd_tupdesc->natts; fc_keyno++)
	{
		Datum		fc_result;
		BrinValues *fc_bval;
		FmgrInfo   *fc_addValue;
		bool		fc_has_nulls;

		fc_bval = &fc_dtup->bt_columns[fc_keyno];

		/*
		 * 该范围是否具有实际的 NULL 值？两个标志都可以被设置，
		 * 但在添加第一行之前我们忽略状态。
		 *
		 * 我们必须记住这一点，因为我们要修改标志， 
		 * 需要知道范围是否开始为空。
		 */
		fc_has_nulls = ((!fc_dtup->bt_empty_range) &&
					 (fc_bval->bv_hasnulls || fc_bval->bv_allnulls));

		/*
		 * 如果我们要添加的值是 NULL，则在本地处理。否则调用 BRIN_PROCNUM_ADDVALUE 过程。
		 */
		if (fc_bdesc->bd_info[fc_keyno]->oi_regular_nulls && fc_nulls[fc_keyno])
		{
			/*
			 * 如果新值为 null，如果是第一个，我们记录它被看到；否则没有其他需做的事情。
			 */
			if (!fc_bval->bv_hasnulls)
			{
				fc_bval->bv_hasnulls = true;
				fc_modified = true;
			}

			continue;
		}

		fc_addValue = index_getprocinfo(fc_idxRel, fc_keyno + 1,
									 BRIN_PROCNUM_ADDVALUE);
		fc_result = FunctionCall4Coll(fc_addValue,
								   fc_idxRel->rd_indcollation[fc_keyno],
								   PointerGetDatum(fc_bdesc),
								   PointerGetDatum(fc_bval),
								   fc_values[fc_keyno],
								   fc_nulls[fc_keyno]);
		/* 如果返回 true，我们需要插入更新的元组 */
		fc_modified |= DatumGetBool(fc_result);

		/*
		 * 如果范围确实具有实际的 NULL 值（即没有从空开始），
		 * 确保我们不忘记 NULL 值。或者 allnulls 标志仍然设置为 true，
		 * 或者（如果操作类清除了它）我们需要设置 hasnulls=true。
		 *
		 * XXX 这仅在操作类修改了元组时发生，因此
		 * 修改标志应该被设置。
		 */
		if (fc_has_nulls && !(fc_bval->bv_hasnulls || fc_bval->bv_allnulls))
		{
			Assert(fc_modified);
			fc_bval->bv_hasnulls = true;
		}
	}

	/*
	 * 在为所有键更新摘要后，标记为非空。
	 *
	 * 如果我们实际上改变了标志值（即元组开始为空），
	 * 我们应该已修改元组。因此我们不应该看到未修改的空范围。
	 */
	Assert(!fc_dtup->bt_empty_range || fc_modified);
	fc_dtup->bt_empty_range = false;

	return fc_modified;
}

static bool fc_check_null_keys(BrinValues *fc_bval, ScanKey *fc_nullkeys, int fc_nnullkeys)
{
	int			fc_keyno;

	/*
	 * 首先检查是否有任何 IS [NOT] NULL 扫描键，并且我们是否违反了它们。
	 */
	for (fc_keyno = 0; fc_keyno < fc_nnullkeys; fc_keyno++)
	{
		ScanKey		fc_key = fc_nullkeys[fc_keyno];

		Assert(fc_key->sk_attno == fc_bval->bv_attno);

		/* 仅处理 IS NULL/IS NOT NULL 测试 */
		if (!(fc_key->sk_flags & SK_ISNULL))
			continue;

		if (fc_key->sk_flags & SK_SEARCHNULL)
		{
			/* IS NULL 扫描键，但范围内没有 NULL */
			if (!fc_bval->bv_allnulls && !fc_bval->bv_hasnulls)
				return false;
		}
		else if (fc_key->sk_flags & SK_SEARCHNOTNULL)
		{
			/*
			 * 对于 IS NOT NULL，我们只能跳过那些已知仅包含
			 * NULL 的范围。
			 */
			if (fc_bval->bv_allnulls)
				return false;
		}
		else
		{
			/*
			 * 既没有使用 IS NULL 也没有使用 IS NOT NULL；假设所有可索引
			 * 的操作符都是严格的，因此在扫描键中使用 NULL 值时返回 false。
			 */
			return false;
		}
	}

	return true;
}
